package primitives;

import light.Light;
import colour.Colour;
import ray.Ray;
import surface.Surface;
import vector.Vector;

/**
 * Contain the element for defining a Triangle. Provide the method for operation
 * on them.
 * 
 * @author Yuting Wu
 * @author Kan Lin
 * 
 */

public class Triangle extends AbstractShape {
	private Vector pTriOrigin;
	private Vector vTriVectorA;
	private Vector vTriVectorB;

	// for the projection of the picture on the triangle
	private double triHeight;
	private double triBottom;
	private double triEdge;
	private int triType;
	private Vector pT1;
	private boolean init = false;
	// A normal(right angle) vector
	private Vector vPn;
	// distance from the origin of the coordinate system to the triangle
	private double vdVal;
	// Distance from intersection point
	private double disHitpoint;
	// position vector of intersection point
	private Vector pHitPoint;
	// direction vector of intersection point
	private Vector vHitPoint;
	// vector between the origin of the triangle and the intersection point
	private Vector vQVal;
	// a-coordinate of the intersection point
	private double pUVal;
	// b-coordinate of the intersection point
	private double pVVal;	
	private double disOritoTri;

	/**
	 * Constructor of Triangle.
	 * 
	 * @param pTriOrigin
	 * @param pOne
	 * @param pTwo
	 * @param surface
	 * @param type
	 */
	public Triangle(final Vector pTriOrigin, final Vector pOne,
			final Vector pTwo, final Surface surface, final int type) {
		super(surface);
		this.pTriOrigin = pTriOrigin;

		if (type == 1) {
			this.vTriVectorA = pOne.subtract(this.pTriOrigin);
			this.vTriVectorB = pTwo.subtract(this.pTriOrigin);
		} else {
			this.vTriVectorA = pOne;
			this.vTriVectorB = pTwo;
		}

	}

	/**
	 * Getter method. It will be used for getting parameter pTriOrigin.
	 * This method also will be used for testing method.
	 * 
	 * @return the pTriOrigin
	 */
	public Vector getTriOrigin() {
		return pTriOrigin;
	}

	/**
	 * Getter method. It will be used for getting parameter vTriVectorA
	 * This method also will be used for testing method.
	 * 
	 * @return the vTriVectorA
	 */
	public Vector getTriVectorA() {
		return vTriVectorA;
	}

	/**
	 * Getter method. It will be used for getting parameter vTriVectorB
	 * This method also will be used for testing method.
	 *  
	 * @return the vTriVectorB
	 */
	public Vector getTriVectorB() {
		return vTriVectorB;
	}

	/**
	 * Getter method. It will be used for getting parameter surface
	 * 
	 * @return the surface
	 */
	public Surface getSurface() {
		return surface;
	}

	public void checkType() {
		// do it when did not before
		if (!init) {

			Vector vT10 = this.getTriVectorA();
			Vector vT20 = this.getTriVectorB();
			pT1 = this.getTriVectorA().add(this.getTriOrigin());

			// the height of the triangle start from Corner one
			final double tAngle = Math.cos(vT10.normalise().dotProduct(
					vT20.normalise()));

			// an obtuse angle
			if (tAngle > Math.PI / 2) {
				triType = 1;
				triEdge = vT10.multiply(-1).dotProduct(vT20.normalise());
				triHeight = Math
						.sqrt(vT10.dotProduct(vT10) - triEdge * triEdge);
				triBottom = Math.sqrt(vT20.dotProduct(vT20));

			} else {
				triType = 2;
				triEdge = vT10.dotProduct(vT20.normalise());
				triHeight = Math
						.sqrt(vT10.dotProduct(vT10) - triEdge * triEdge);
				triBottom = Math.sqrt(vT20.dotProduct(vT20));
			}
			init = true;
		}
	}

	/**
	 * Getter method. It will be used for getting parameter triType
	 * 
	 * @return the triType
	 */
	public int getTriType() {
		return triType;
	}

	/**
	 * Setter method. Because there are two types of triangle construction. this
	 * method will be used for setting triangle type
	 * 
	 * @param triType
	 *            the triType to set
	 */
	public void setTriType(int triType) {
		this.triType = triType;
	}

	/**
	 * @return the triHeight
	 */
	public double getTriHeight() {
		return triHeight;
	}

	/**
	 * @return the triBottom
	 */
	public double getTriBottom() {
		return triBottom;
	}

	/**
	 * @return the triEdge
	 */
	public double getTriEdge() {
		return triEdge;
	}

	/**
	 * @return the pT1
	 */
	public Vector getpT1() {
		return pT1;
	}

	/**
	 * calculate the cos value for two vector
	 * 
	 * @param vLine1
	 * @param vLine2
	 * @return cosValue
	 */
	private double angleCal(final Vector vLine1, final Vector vLine2) {
		return Math.acos(vLine1.normalise().dotProduct(vLine2.normalise()));
	}

	/**
	 * Calculate the colour on the surface of triangle according to the position
	 * of hitpoint
	 */
	@Override
	public Colour getColorAt(Vector pHitPoint, Vector vHitPoint) {
		Colour result;
		if (surface == null) {
			result = new Colour(0, 0, 0);
			System.out.println("no surface");
		} else {
			if (surface.getType() == 1) {
				result = this.surface.getColour();
			} else if (surface.getType() == 0) {
				result = new Colour(0, 0, 0);
			} else {
				this.checkType();
				double vTransform;
				double uTransform;

				// seting the value of Trangle including the height and corner
				// position
				Vector vT20 = this.getTriVectorB();
				Vector pT1 = this.getpT1();

				if (triType == 1) {
					final Vector vT1i = pT1.subtract(pHitPoint);
					final double d2Val = vT1i.multiply(-1).dotProduct(
							vT20.normalise());
					final double h1Val = Math.sqrt(vT1i.dotProduct(vT1i)
							- d2Val * d2Val);
					vTransform = 1 - h1Val / triHeight;
					uTransform = (d2Val / (1 - vTransform) - triEdge)
							/ triBottom;
				} else {

					// It is position of hitpoint
					final Vector vT1i = pT1.subtract(pHitPoint);
					final double angleT1i = angleCal(vT1i, vT20);

					// When angle between the intersection point and bottom is
					// lagre
					// then 90
					if (angleT1i > Math.PI / 2) {
						final double d2Val = vT1i.multiply(-1).dotProduct(
								vT20.normalise());
						final double h1Val = Math.sqrt(vT1i.dotProduct(vT1i)
								- d2Val * d2Val);
						vTransform = 1 - h1Val / triHeight;

						uTransform = (d2Val / (1 - vTransform) + triEdge)
								/ triBottom;
					} else {
						final double d2Val = vT1i.dotProduct(vT20.normalise());
						final double h1Val = Math.sqrt(vT1i.dotProduct(vT1i)
								- d2Val * d2Val);

						vTransform = 1 - h1Val / triHeight;

						if (triEdge < triBottom) {

							uTransform = (triEdge - d2Val / (1 - vTransform))
									/ triBottom;
						} else {
							uTransform = 1
									- (d2Val
											/ ((1 - vTransform) - (triEdge - triBottom)) * triEdge)
									/ triBottom;
						}
					}

				}
				result = surface.getColourIn(uTransform, vTransform);

			}
		}
		return result;

	}

	/**
	 * Get the default diffuse value. If there is no information about diffuse
	 * value in xml file, it will be set to 1.0
	 * 
	 * @return the default diffuse value.
	 */
	@Override
	public double getDiffuse() {
		// set the default
		double diffuse;
		if (surface != null) {
			diffuse = this.surface.getDiffuse();
		} else {
			diffuse = 1.0;
		}

		return diffuse;
	}

	/**
	 * Get the default reflect value. If there is no information about reflect
	 * value in xml file, it will be set to 0.0
	 * 
	 * @return the default reflect value.
	 */
	@Override
	public double getReflect() {
		double reflect;
		if (surface != null) {
			reflect = this.surface.getReflect();
		} else {
			reflect = 0.0;
		}

		return reflect;

	}

	/**
	 * Override toString method.
	 * 
	 * @return all the information from a Triangle
	 */
	public String toString() {
		return "Triangle:One: " + this.getTriOrigin() + ",Two: "
				+ this.getTriVectorA() + ",Three: " + this.getTriVectorB()
				+ ",Surface: " + this.getSurface();
	}

	/**
	 * Compute related computation of triangle. After computation, it will
	 * return a boolean value.
	 * 
	 * @return a boolean value.
	 */
	@Override
	public boolean calMutiIntersection(Ray raylist, Boolean ifSimple) {
		disOritoTri = -1;
		vdVal = calVd(raylist);
		if (vdVal != 0) {
			calDishitpoint(raylist);
			if (disHitpoint >= 0) {
				this.calPosHitpoint(raylist);
				this.calculateQ();
				this.calculateU();
				if ((this.pUVal > 0) && (this.pUVal < 1)) {
					this.calculateV();
					if (((this.pVVal > 0) && (this.pVVal < 1))
							&& (this.pUVal + this.pVVal <= 1)) {
						return true;
					}
				}
			}
		}
		return false;
	}

	/**
	 * Getter method. Get distance from hit point
	 * 
	 * @return the distance from hit point
	 */
	@Override
	public double getDisHitPointIndex() {
		return disHitpoint;
	}

	/**
	 * Getter method. get the position vector of hit point
	 * 
	 * @return the position vector of hit point
	 */
	@Override
	public Vector getPositionHitPointIndex() {
		return pHitPoint;
	}

	/**
	 * Getter method. get the direction vector of hit point
	 * 
	 * @return the direction vector of hit point
	 */
	@Override
	public Vector getVectorHitPointIndex() {
		return vHitPoint;
	}

	/**
	 * This method is mainly used for Sphere.
	 */
	@Override
	public boolean isInsideOrOutside() {
		return false;
	}

	/**
	 * Compute further calculation for triangle
	 * 
	 * @param rayInput
	 *            input ray
	 */
	@Override
	public void runFurtherCalculation(Ray rayInput) {
		if ((vdVal != 0 && disHitpoint >= 0)
				&& ((this.pVVal > 0) && (this.pVVal < 1))
				&& (this.pUVal + this.pVVal <= 1)) {
			this.calNormal(rayInput);
		}

	}

	/**
	 * Compute the normal vector to the triangle's plane Compute the distance
	 * from the origin of the coordinate system (0,0,0) to the plane
	 * 
	 * @param rayInput
	 *            Input ray
	 * @return computed result
	 */
	private double calVd(final Ray rayInput) {
		final Vector temp = getTriVectorA().crossProduct(getTriVectorB());
		vPn = temp.normalise();
		disOritoTri = getTriOrigin().dotProduct(vPn);
		return (vPn.dotProduct(rayInput.getDesiredRay()));
	}

	/**
	 * Comput the distance
	 * 
	 * @param rayInput
	 *            Input ray
	 * @return the computed result
	 */
	private double calDishitpoint(final Ray rayInput) {
		double v0Val = disOritoTri - vPn.dotProduct(rayInput.getOrigin());
		disHitpoint = v0Val / this.vdVal;
		return disHitpoint;
	}

	/**
	 * Compute the intersection point
	 * 
	 * @param rayInput
	 *            Input ray
	 * @return location of the intersection point
	 */
	private Vector calPosHitpoint(final Ray rayInput) {
		this.pHitPoint = rayInput.getOrigin().add(
				rayInput.getDesiredRay().multiply(disHitpoint));
		return pHitPoint;
	}

	/**
	 * Compute the normal vector
	 * 
	 * @param rayInput
	 * @return computed results
	 * 
	 */
	private Vector calNormal(final Ray rayInput) {
		if (this.vdVal < 0) {
			this.vHitPoint = this.vPn;
		} else {
			this.vHitPoint = this.vPn.multiply(-1);
		}
		return this.vHitPoint;
	}

	/**
	 * Compute the vector between the origin of the triangle and the
	 * intersection point.
	 * 
	 * @return computed results
	 */
	private Vector calculateQ() {
		this.vQVal = this.pHitPoint.subtract(this.pTriOrigin);
		return vQVal;
	}

	/**
	 * Compute the a-coordinate of the intersection point
	 * 
	 * @return computed results
	 */
	private double calculateU() {
		final double temp1 = ((vTriVectorB.dotProduct(vTriVectorB)) * (this.vQVal
				.dotProduct(vTriVectorA)))
				- ((vTriVectorA.dotProduct(vTriVectorB)) * (this.vQVal
						.dotProduct(vTriVectorB)));
		final double temp2 = ((vTriVectorA.dotProduct(vTriVectorA)) * (vTriVectorB
				.dotProduct(vTriVectorB)))
				- ((vTriVectorA.dotProduct(vTriVectorB)) * (vTriVectorA
						.dotProduct(vTriVectorB)));
		this.pUVal = temp1 / temp2;
		return pUVal;
	}

	/**
	 * Compute the b-coordinate of the intersection point
	 * 
	 * @return computed results
	 */
	private double calculateV() {
		final double temp1 = (this.vQVal.dotProduct(this.vTriVectorB))
				- this.pUVal * (this.vTriVectorA.dotProduct(this.vTriVectorB));
		final double temp2 = this.vTriVectorB.dotProduct(this.vTriVectorB);
		this.pVVal = temp1 / temp2;
		return pVVal;
	}

	@Override
	/**
	 * calculate the intensity of the light through the angle of light and
	 * normal of the hitpoint
	 */
	public double lightIntensityCalculation(Light singleLight,Vector pHitPoint,Vector vHitPoint) {
		Vector s0 = pHitPoint.add(vHitPoint.multiply(0.0001));
		Vector sd = singleLight.getLight().subtract(s0).normalise();
		return Math.abs(vHitPoint.dotProduct(sd))*this.getDiffuse();
	}

}
